// TODO: CODE REVIEW & CLEANUP!
//
// TouchListBox.cs
//
// Implements TouchListBox and related types.
//

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using HomeUX.ClientControlSupport;
using HomeUX.Synchronization;
using HomeUX.UiUtilities;
using HomeUX.Utilities;
using SyncPathClass = HomeUX.Synchronization.SyncPath;
using System.ComponentModel;

namespace HomeUX.Controls
{

/// <summary>
/// Only for internal use.
/// </summary>
public abstract class TouchSelector : ItemsControl, IControlHelperConnector
{
    public static readonly DependencyProperty IndexLetterProperty =
        DependencyProperty.RegisterAttached("IndexLetter", typeof(char),
        typeof(TouchSelector), null);

    public static char GetIndexLetter(FrameworkElement element)
    {
        return (char)element.GetValue(IndexLetterProperty);
    }

    public static void SetIndexLetter(FrameworkElement element, char value)
    {
        element.SetValue(IndexLetterProperty, value);
    }

    protected SelectionMode _selectionMode;
	//protected bool _isMultiSelect;

    public static readonly DependencyProperty SelectionModeProperty =
        DependencyProperty.Register("SelectionMode", typeof(SelectionMode), typeof(TouchSelector),
        new PropertyMetadata(TouchSelector.OnSelectionModePropertyChanged));

    public static readonly DependencyProperty DragZoneVisibilityProperty =
        DependencyProperty.Register("DragZoneVisibility", typeof(DragZoneVisibility), typeof(TouchSelector),
		new PropertyMetadata(TouchSelector.OnDragZoneVisibilityPropertyChanged));

    public static readonly DependencyProperty ScrollButtonVisibilityProperty =
        DependencyProperty.Register("ScrollButtonVisibility", typeof(UiVisibility),
		typeof(TouchSelector), null);

    public static readonly DependencyProperty ScrollSliderVisibilityProperty =
        DependencyProperty.Register("ScrollSliderVisibility", typeof(UiVisibility),
		typeof(TouchSelector), null);

    public static readonly DependencyProperty ScrollPeekAmountProperty =
        DependencyProperty.Register("ScrollPeekAmount", typeof(double), typeof(TouchSelector),
        null);

    public static readonly DependencyProperty ListMarginProperty =
        DependencyProperty.Register("ListMargin", typeof(Thickness), typeof(TouchSelector),
        new PropertyMetadata(TouchSelector.OnListMarginPropertyChanged));

    public static readonly DependencyProperty ItemStyleProperty =
        DependencyProperty.Register("ItemStyle", typeof(Style), typeof(TouchSelector), null);

    public static readonly DependencyProperty ItemButtonStyleProperty =
        DependencyProperty.Register("ItemButtonStyle", typeof(Style), typeof(TouchSelector), null);

    public static readonly DependencyProperty ViewportMinWidthProperty =
        DependencyProperty.Register("ViewportMinWidth", typeof(double), typeof(TouchSelector), null);

    public static readonly DependencyProperty ViewportMinHeightProperty =
        DependencyProperty.Register("ViewportMinHeight", typeof(double), typeof(TouchSelector), null);

    public static readonly DependencyProperty ViewportMaxHeightProperty =
        DependencyProperty.Register("ViewportMaxHeight", typeof(double), typeof(TouchSelector), null);

    public static readonly DependencyProperty ViewportMaxWidthProperty =
        DependencyProperty.Register("ViewportMaxWidth", typeof(double), typeof(TouchSelector), null);

    public static readonly DependencyProperty PageButtonVisibilityProperty =
        DependencyProperty.Register("PageButtonVisibility", typeof(Visibility), typeof(TouchSelector), null);

    public static readonly DependencyProperty SyncPathProperty =
        DependencyProperty.Register("SyncPath", typeof(string), typeof(TouchSelector),
            new PropertyMetadata(TouchSelector.OnSyncPathPropertyChanged));

    public static readonly DependencyProperty ConfigurationSyncPathProperty =
        DependencyProperty.Register("ConfigurationSyncPath", typeof(string), typeof(TouchSelector),
            new PropertyMetadata(TouchSelector.OnConfigurationSyncPathPropertyChanged));

    public static readonly DependencyProperty ItemsConfigurationSyncPathProperty =
        DependencyProperty.Register("ItemsConfigurationSyncPath", typeof(string), typeof(TouchSelector),
            new PropertyMetadata(TouchSelector.OnItemsConfigurationSyncPathPropertyChanged));

    public static readonly DependencyProperty ItemActionProperty =
        DependencyProperty.Register("ItemAction", typeof(ItemAction), typeof(TouchSelector), null);

    public static readonly DependencyProperty SelectedItemValueProperty =
        DependencyProperty.Register("SelectedItemValue", typeof(object), typeof(TouchSelector),
            new PropertyMetadata(TouchSelector.OnSelectedItemValuePropertyChanged));

    public static readonly DependencyProperty IsWaitingProperty =
        DependencyProperty.Register("IsWaiting", typeof(bool), typeof(TouchSelector),
            new PropertyMetadata(TouchSelector.OnIsWaitingPropertyChanged));

    //public static readonly DependencyProperty IsMultiSelectProperty =
    //    DependencyProperty.Register("IsMultiSelect", typeof(bool), typeof(TouchSelector),
    //        new PropertyMetadata(TouchSelector.OnIsMultiSelectPropertyChanged));

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public SelectionMode SelectionMode
    {
        get
        {
            return (SelectionMode)GetValue(SelectionModeProperty);
        }
        set
        {
            SetValue(SelectionModeProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public DragZoneVisibility DragZoneVisibility
    {
        get
        {
            return (DragZoneVisibility)GetValue(DragZoneVisibilityProperty);
        }
        set
        {
            SetValue(DragZoneVisibilityProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public UiVisibility ScrollButtonVisibility
    {
        get
        {
            return (UiVisibility)GetValue(ScrollButtonVisibilityProperty);
        }
        set
        {
            SetValue(ScrollButtonVisibilityProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public UiVisibility ScrollSliderVisibility
    {
        get
        {
            return (UiVisibility)GetValue(ScrollSliderVisibilityProperty);
        }
        set
        {
            SetValue(ScrollSliderVisibilityProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public double ScrollPeekAmount
    {
        get
        {
            return (double)GetValue(ScrollPeekAmountProperty);
        }
        set
        {
            SetValue(ScrollPeekAmountProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public Thickness ListMargin
    {
        get
        {
            return (Thickness)GetValue(ListMarginProperty);
        }
        set
        {
            SetValue(ListMarginProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public Style ItemStyle
    {
        get
        {
            return (Style)GetValue(ItemStyleProperty);
        }
        set
        {
            SetValue(ItemStyleProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public Style ItemButtonStyle
    {
        get
        {
            return (Style)GetValue(ItemButtonStyleProperty);
        }
        set
        {
            SetValue(ItemButtonStyleProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public double ViewportMinWidth
    {
        get
        {
            return (double)GetValue(ViewportMinWidthProperty);
        }
        set
        {
            SetValue(ViewportMinWidthProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public double ViewportMinHeight
    {
        get
        {
            return (double)GetValue(ViewportMinHeightProperty);
        }
        set
        {
            SetValue(ViewportMinHeightProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public double ViewportMaxWidth
    {
        get
        {
            return (double)GetValue(ViewportMaxWidthProperty);
        }
        set
        {
            SetValue(ViewportMaxWidthProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public double ViewportMaxHeight
    {
        get
        {
            return (double)GetValue(ViewportMaxHeightProperty);
        }
        set
        {
            SetValue(ViewportMaxHeightProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public Visibility PageButtonVisibility
    {
        get
        {
            return (Visibility)GetValue(PageButtonVisibilityProperty);
        }
        set
        {
            SetValue(PageButtonVisibilityProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public string SyncPath
    {
        get
        {
            return (string)GetValue(SyncPathProperty);
        }
        set
        {
            SetValue(SyncPathProperty, value);
        }
    }

    // the sync path, relative to "Configuration\<r>SyncPath</r>", where configuration information is stored for this
    // control; <r>ItemsConfigurationSyncPath</r> is relative to this

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public string ConfigurationSyncPath
    {
        get
        {
            return (string)GetValue(ConfigurationSyncPathProperty);
        }
        set
        {
            SetValue(ConfigurationSyncPathProperty, value);
        }
    }

	/// <summary>
	/// The path to the <r>SyncNode</r> whose children define the items to display in the list box.
	/// This path is relative to <r>ConfigurationSyncPath</r>.  If <n>null</n>,
	/// the list of items is not bound to the SyncTree.
	/// </summary>
    ///
    /// <remarks>
    /// <para>
    /// When <r>ItemsConfigurationSyncPath</r> is not <n>null</n>, the list box items contain
    /// <n>TextBlock</n> objects (with the style specified by <r>ItemStyle</r>, if not
    /// <n>null</n>).  The text of each <n>TextBlock</n> corresponds to the <s>SyncNode.Value</s>
    /// of each <r>SyncNode</r> item that's a child of
    /// "Configuration/<r>SyncPath</r>/<r>ItemsConfigurationSyncPath</r>".  The
	/// <r cref="Item.DataProperty">Data</r> of
    /// each item is the child <r>SyncNode</r>'s SyncNode.Name--?? TODO.
    /// </para>
    /// <para>
    /// If a list box item <r>SyncNode</r> needs to be deleted on the server, deleting the
    /// <r>SyncNode</r> itself doesn't work, since <r>SyncNode</r> deletions are not propagated to
    /// clients.  Instead, <r>TouchSelector</r> follows the convention that list box item
    /// <r>SyncNode</r> objects that have <s>SyncNode.Value</s> equal to <n>null</n> are treated
    /// as deleted.
    /// </para>
    /// </remarks>
    ///
    public string ItemsConfigurationSyncPath
    {
        get
        {
            return (string)GetValue(ItemsConfigurationSyncPathProperty);
        }
        set
        {
            SetValue(ItemsConfigurationSyncPathProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public ItemAction ItemAction
    {
        get
        {
            return (ItemAction)GetValue(ItemActionProperty);
        }
        set
        {
            SetValue(ItemActionProperty, value);
        }
    }

    /// <summary>
    /// The "item value" (i.e. the value specified by the <r cref="Item.DataProperty">Item.Data</r>
	/// property attached to on the item, as interpreted by
    /// <r>ItemAction</r>) of the list box item that should be selected (i.e. lit up).  If no
    /// list box item has this item value, then nothing is selected.  <r>SelectedItemValue</r> may
    /// be set to a value before an item with that value is added to the list box -- at the time
    /// the item is added it will be selected.  If <r>SelectedItemValue</r> is <n>null</n>, no
    /// item is selected.
    /// </summary>
    public object SelectedItemValue
    {
        get
        {
            return GetValue(SelectedItemValueProperty);
        }
        set
        {
            SetValue(SelectedItemValueProperty, value);
        }
    }

    /// <summary>
    /// TODO 
    /// </summary>
    ///
    public bool IsWaiting
    {
        get
        {
            return (bool)GetValue(IsWaitingProperty);
        }
        set
        {
            SetValue(IsWaitingProperty, value);
        }
    }

    /// <summary>
    /// Fired when some or all items have been added to the list box.  At this point,
    /// <n>TryMapItemToButton</n> and <n>TryMapItemValueToButton</n> will work for those items.
    /// </summary>
    public event EventHandler ItemsAdded;

    protected void FireItemsAdded()
    {
        if (ItemsAdded != null)
            ItemsAdded(this, EventArgs.Empty);
    }

    // <summary>
    // How multi-select works: set IsMultiSelect="True", ItemAction="ToggleBooleanValue",
    // ItemsConfigurationSyncPath to whatever provides item names (e.g. "." in the case of SyncPath="DeviceStatus"),
    // and then, in multi-select mode, SelectedItemValue is not used, and instead each button (item) is lit or
    // not depending on whether the SyncNode at path "(SyncPath)\(item-name)" has a value of true or false.
    // </summary>
    //public bool IsMultiSelect
    //{
    //    get
    //    {
    //        return (bool)GetValue(IsMultiSelectProperty);
    //    }
    //    set
    //    {
    //        SetValue(IsMultiSelectProperty, value);
    //    }
    //}

    static void OnSyncPathPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target.OnSyncPathChanged((string)e.OldValue, (string)e.NewValue);
    }

    protected virtual void OnSyncPathChanged(string oldValue, string newValue)
    {
    }

    static void OnConfigurationSyncPathPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target.OnConfigurationSyncPathChanged((string)e.OldValue, (string)e.NewValue);
    }

    protected virtual void OnConfigurationSyncPathChanged(string oldValue, string newValue)
    {
    }

    static void OnItemsConfigurationSyncPathPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target.OnItemsConfigurationSyncPathChanged((string)e.OldValue, (string)e.NewValue);
    }

    protected virtual void OnItemsConfigurationSyncPathChanged(string oldValue, string newValue)
    {
    }

    static void OnListMarginPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target.OnListMarginChanged((Thickness)e.OldValue, (Thickness)e.NewValue);
    }

    protected virtual void OnListMarginChanged(Thickness oldValue, Thickness newValue)
    {
    }

    static void OnSelectionModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target._selectionMode = (SelectionMode)e.NewValue;
    }

    static void OnDragZoneVisibilityPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target.OnDragZoneVisibilityChanged((DragZoneVisibility)e.OldValue, (DragZoneVisibility)e.NewValue);
    }

    protected virtual void OnDragZoneVisibilityChanged(DragZoneVisibility oldValue, DragZoneVisibility newValue)
    {
    }

    static void OnSelectedItemValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target.OnSelectedItemValueChanged(e.OldValue, e.NewValue);
    }

    protected virtual void OnSelectedItemValueChanged(object oldValue, object newValue)
    {
    }

    static void OnIsWaitingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchSelector)d;
        target.OnIsWaitingChanged((bool)e.OldValue, (bool)e.NewValue);
    }

    protected virtual void OnIsWaitingChanged(bool oldValue, bool newValue)
    {
    }

    //static void OnIsMultiSelectPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    //{
    //    var target = (TouchSelector)d;
    //    target._isMultiSelect = (bool)e.NewValue;
    //}

    // IControlHelperConnector implementation

    Control IControlHelperConnector.GetControl()
    {
        return this;
    }

    DependencyObject IControlHelperConnector.GetTemplateChild(string childName)
    {
        return GetTemplateChild(childName);
    }
}

/// <summary>
/// TODO
/// </summary>
/// 
/// <remarks>
/// 
/// [See also: description in TouchDropDownList]
/// Items may be populated (a) statically, by children of the "&lt;TouchListBox&gt;" element; (b) through
/// code, by updating the <n>Items</n> collection (see <n>ItemsControl</n>, which <r>TouchListBox</r> is
/// based on), or (c) by binding to the Configuration portion of the SyncTree -- see
/// <ar>ItemsConfigurationSyncPath</ar>.
/// 
/// In cases (a) and (b), use the <r cref="Item.DataProperty">Item.Data</r> attached property to specify
/// data for each item.
/// 
/// <para>
/// You should always set either <n>Width</n> or <n>ViewportMinWidth</n> to the desired width (of the
/// entire control, or the viewport -- i.e. the rectangle in which scrolling occurs, respectively),
/// since <r>TouchListBox</r> uses this information to figure out how many items can fit on each row.
/// 
/// At that point, by default, the height of the list box will automatically be calculated as the height
/// needed for all rows.  If you want to force scrolling after a certain height, set <n>Height</n> or
/// <ar>ViewportMaxHeight</ar>.  If you want to prevent the viewport from being smaller than a certain
/// height, set <ar>ViewportMinHeight</ar>.
/// 
/// 
/// </para>
/// 
/// </remarks>
/// 
[TemplatePart(Name = LayoutRootName, Type = typeof(TouchListBoxScrollViewer))]
[TemplatePart(Name = ExtentName, Type = typeof(FrameworkElement))]
[TemplatePart(Name = GripBlockContainerName, Type = typeof(Canvas))]
[TemplatePart(Name = RowContainerName, Type = typeof(Canvas))]
public class TouchListBox : TouchSelector
{
    const string LayoutRootName = "LayoutRoot";
    const string ExtentName = "Extent";
    const string GripBlockContainerName = "GripBlockContainer";
    const string RowContainerName = "RowContainer";

    public static readonly DependencyProperty SuppressFocusDisplayProperty =
        DependencyProperty.Register("SuppressFocusDisplay", typeof(bool), typeof(TouchListBox), null);

    public event IsWaitingChangedEventHandler IsWaitingChanged;

    public event EventHandler SelectedItemValueChanged;

    public event ItemClickEventHandler ItemClick;

    TouchListBoxScrollViewer _layoutRoot;
    FrameworkElement _extent;
    Canvas _gripBlockContainer;
    Canvas _rowContainer;
    Style _gripBlockStyle;
    //Style _itemButtonStyle;

    /// <summary>
    /// Implements the connection between this control and the global SyncTree object, which in
    /// turn manages the connection to the server.
    /// </summary>
    SyncHelper _syncHelper;

	//TouchListBoxItem _selectedContainer;

    Dictionary<object, TouchListBoxItem> _mapItemToContainer = new Dictionary<object, TouchListBoxItem>();
    Dictionary<object, TouchListBoxItem> _mapItemValueToContainer = new Dictionary<object, TouchListBoxItem>();

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="item">TODO</param>
    ///
    /// <param name="button">TODO</param>
    ///
    public bool TryMapItemToButton(object item, out TouchListBoxItem button)
    {
        return _mapItemToContainer.TryGetValue(item, out button);
    }

    /// <summary>
    /// TODO
    /// </summary>
    /// 
    /// <param name="itemValue">TODO</param>
    /// 
    /// <param name="button">TODO</param>
    ///
    public bool TryMapItemValueToButton(object itemValue, out TouchListBoxItem button)
    {
        return _mapItemValueToContainer.TryGetValue(itemValue, out button);
    }

    /// <summary>
    /// Gets or sets a value which indicates if display of overall focus to the control is
    /// enabled.
    /// </summary>
    public bool SuppressFocusDisplay
    {
        get
        {
            return (bool)GetValue(SuppressFocusDisplayProperty);
        }
        set
        {
            SetValue(SuppressFocusDisplayProperty, value);
        }
    }

    public TouchListBox() 
    {
        DefaultStyleKey = typeof(TouchListBox);

        _syncHelper = new SyncHelper(this, SyncHelperFeatures.Configuration);

        // hook up <_syncHelper> events
        _syncHelper.IsOnlineChanged += _syncHelper_IsOnlineChanged;
        _syncHelper.IsWaitingChanged += _syncHelper_IsWaitingChanged;
        _syncHelper.PrimaryValueChanged += _syncHelper_PrimaryValueChanged;
        _syncHelper.ConfigurationChanged += _syncHelper_ConfigurationChanged;
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        ControlHelper.GetRequiredTemplateChild(this, LayoutRootName, out _layoutRoot);
        ControlHelper.GetRequiredTemplateChild(this, ExtentName, out _extent);
        ControlHelper.GetRequiredTemplateChild(this, GripBlockContainerName, out _gripBlockContainer);
        ControlHelper.GetRequiredTemplateChild(this, RowContainerName, out _rowContainer);

        _layoutRoot.RowContainer = _rowContainer;

        //_extent.SizeChanged += delegate
        //{
        //    if (Items.Count == _mapItemToContainer.Count)
        //    {
        //        if (LayoutComplete != null)
        //            LayoutComplete(this, EventArgs.Empty);
        //    }
        //};
        
        //ItemsPresenter _itemsPresenter;
        //ControlHelper.GetRequiredTemplateChild(this, "ItemsPresenter", out _itemsPresenter);

        //ControlHelper.GetRequiredResource(_extent, "RowStyle", out _rowStyle);
        UiUtil.GetRequiredResource(_extent, "GripBlockStyle", out _gripBlockStyle);
        //ControlHelper.GetRequiredResource(_extent, "ItemButtonStyle", out _itemButtonStyle);

        //_rowContainer.Children.Clear();
        //double availableWidth = _rowContainer.ActualWidth;

        // NOTE: ListBox can have the concept of selection, or not <-- have a SelectMode property
        // with values NoSelection (i.e. don't light up button, just want a verb -- e.g. IR transmit),
        // SingleSelection (unselect previous selected item), MultipleSelection

        // We want a TouchListBoxItem class; each item can have content (object, used as content of
        // TouchListBoxItem) plus its own item data
        // (TouchListBox has SyncPath and ItemAction properties...?)

        SizeChanged += delegate { PerformLayoutIfExtentWidthChanged(); };

        IsEnabledChanged += delegate
        {
            bool isEnabled = IsEnabled;
            foreach (GripBlock gripBlock in _gripBlockContainer.Children)
            {
                gripBlock.IsEnabled = isEnabled;
            }
        };

        // check if this control is an orphan, now and when Parent may change
        _syncHelper.CheckOrphanStatus(Parent);
        LayoutUpdated += (sender, e) => _syncHelper.CheckOrphanStatus(Parent);

        // in case this control is being created after the configuration node has already been
        // synchronized from the server, then if this control gets its items populated from the
        // configuration, do so now
        PopulateItemsFromConfiguration();
    }

    /// <summary>
    /// Called when <r>_syncHelper</r>.<s>SyncHelper.PrimaryNode</s><s>SyncNodeBase.Value</s> has changed -- for example, if another client has
    /// changed the value.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_PrimaryValueChanged(object sender, ValueChangedEventArgs e)
    {
        // ignore this event if the change originated from this control
        if (e.Originator == this)
            return;

        // select (i.e. light up) the list box item that has an item value as specified
        // by <_syncHelper.PrimaryNode.Value>
        SelectedItemValue = _syncHelper.PrimaryNode.Value;
    }

    /// <summary>
    /// Called when <r>_syncHelper</r>.<s>SyncNode.IsWaiting</s> has changed.  This occurs when
    /// <r>_syncHelper</r><s>SyncHelper.PrimaryNode</s><s>SyncNodeBase.Value</s> is changed on this client, but the change has not yet been
    /// reflected on the server.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_IsWaitingChanged(object sender, EventArgs e)
    {
        // show or hide the wait icon (e.g. hourglass)
        IsWaiting = _syncHelper.IsWaiting;
    }

    /// <summary>
    /// Called when <r>_syncHelper</r>.<s>SyncHelper.IsOnline</s> has changed.  This occurs when the connection to
    /// the server has been broken or reestablished.  This also occurs when
    /// <r>_syncHelper</r><s>SyncHelper.PrimaryNode</s><s>SyncNodeBase.Value</s> becomes <n>null</n> (indicating, for example, that the device
    /// associated with that value is offline) or becomes non-<n>null</n>.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_IsOnlineChanged(object sender, EventArgs e)
    {
        // enable or disable the control
        IsEnabled = _syncHelper.IsOnline;
    }

    /// <summary>
    /// Called when a descendant of <r>_syncHelper</r>.<s>SyncHelper.ConfigurationNode</s> has changed -- for
    /// example, if another client has change the configuration information.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_ConfigurationChanged(object sender, DescendantChangedEventArgs e)
    {
        // ignore this event if the change originated from this control
        if (e.Originator == this)
            return;

        // load items from the configuration node
        PopulateItemsFromConfiguration();
    }

    /// <summary>
    /// Loads this controls items from its configuration node, if this control's list of items is
    /// bound to the SyncTree.
    /// </summary>
    ///
    void PopulateItemsFromConfiguration()
    {
        // do nothing if the list of items is not bound to the SyncTree
        if (ItemsConfigurationSyncPath == null)
            return;

        // do nothing if the configuration information is not currently available
        if (!_syncHelper.CheckAccess(SyncHelperFeatures.Configuration))
            return;

        // do nothing in design mode
        if (DesignerProperties.IsInDesignTool)
            return;

        // set <configurationSyncPath> to the path to the SyncNode whose children contain the items
        // to display, or null if the list box items aren't bound to the SyncTree
        SyncPath configurationSyncPath = SyncPathClass.FromString(ItemsConfigurationSyncPath);
        if (configurationSyncPath == null)
            return;

        // clear <Items>, then add the names of the children of <configurationSyncPath>
        Items.Clear();
        Style itemStyle = ItemStyle;
        foreach (SyncNode itemNode in _syncHelper.ConfigurationNode[configurationSyncPath].Children)
        {
            // ignore a SyncNode that has no value, since that's the convention that TouchListBox
            // uses for marking a previously-synchronized SyncNode as being deleted
            if (itemNode.Value == null)
                continue;

			// set <item> to a new TextBlock whose Text property is the label for the new item
			// (determined by the <itemNode.Value>) and attach a Item.Data
			// property with the item data
            var item = new TextBlock()
            {
                Text = itemNode.Value.ToString()
            };
            //item.SetValue(Item.DataProperty, itemNode.Name);
            Item.SetUntypedValue(item, itemNode.Name);

			// if ItemStyle is specified, use it
            if (itemStyle != null)
                item.Style = itemStyle;

			// add <item> to the collection of items
            Items.Add(item);
        }
    }

    bool _pendingLayout;

    void PerformLayoutAsync()
    {
        if (_pendingLayout)
            return;
        Dispatcher.BeginInvoke(PerformLayout);
        _pendingLayout = true;
    }

    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        switch (e.Action)
        {

        case NotifyCollectionChangedAction.Reset:

            _mapItemToContainer.Clear();
            _mapItemValueToContainer.Clear();

            if ((_layoutRoot != null) && (_layoutRoot.PseudofocusedItem != null))
            {
                // we're removing the item with pseudofocus -- notify <_layoutRoot>
                //_layoutRoot.PseudofocusedItem = null;
                _layoutRoot.TrySetPseudofocus(null);
            }
            break;

        case NotifyCollectionChangedAction.Replace:
        case NotifyCollectionChangedAction.Remove:

            ItemAction itemAction = ItemAction;
            if (e.OldItems != null)
            {
                IPseudofocusable pseudofocusedItem = _layoutRoot.PseudofocusedItem;
                foreach (object item in e.OldItems)
                {
                    TouchListBoxItem container = _mapItemToContainer[item];
                    if (container == pseudofocusedItem)
                    {
                        // we're removing the item with pseudofocus -- notify <_layoutRoot>
                        //_layoutRoot.PseudofocusedItem = null;
                        _layoutRoot.TrySetPseudofocus(null);
                    }
                    if (container.ItemValue != null)
                        _mapItemValueToContainer.Remove(container.ItemValue);
                    _mapItemToContainer.Remove(item);
                }
            }
            break;
        }
    }

    protected override void PrepareContainerForItemOverride(DependencyObject containerElement, object item)
    {
        // let the base class do its thing
        base.PrepareContainerForItemOverride(containerElement, item);

        // set <container> to the item container that was created in GetContainerForItemOverride
        // for display of <item>
        var container = (TouchListBoxItem)containerElement;
        container.Style = ItemButtonStyle;
        container.Content = item;
        if (_selectionMode != SelectionMode.MultiSelect)
            container.Click += delegate { OnItemClick(item, container); };
        container.Visibility = Visibility.Collapsed;

		// update <_mapItemToContainer> to map <item> to its item container
        _mapItemToContainer.Add(item, container);

		// set <data> to the item data of <item>, or null if <item> doesn't have item data
		// specified
        string data = Item.GetUntypedValue(item);

        if (_selectionMode == SelectionMode.MultiSelect)
        {
            container.SyncPath = SyncPathClass.Build(SyncPath, data).ToString();
            container.ItemAction = ItemAction.ToggleBooleanValue;
        }

		// set <itemValue> to the deserialized item value corresponding to <data>,
		// or null if <data> doesn't correspond to a valid item value
        object itemValue = Item.UntypedValueToTypedValue(ItemAction, data);

		// store <itemValue> in the item container for fast future access
		container.ItemValue = itemValue;

        // if this item is supposed to be displayed as selected, do so
        if ((Object.Equals(itemValue, SelectedItemValue) && (itemValue != null)) &&
            (_selectionMode != SelectionMode.MultiSelect))
                container.IsLit = true;

		// if <itemValue> is specified, update <_mapItemValueToContainer> to map it to the item
		// container
        if (itemValue != null)
        {
            _mapItemValueToContainer[itemValue] = container;

            if (_mapItemToContainer.Count == Items.Count)
                FireItemsAdded();
        }

        PerformLayoutAsync();

        //if (_mapItemToContainer.Count == Items.Count)
        //{
        //    if (ItemsAdded != null)
        //        ItemsAdded(this, EventArgs.Empty);
        //}
    }

    // <summary>
    // Fired after a batch of items have been added.  At this point, TryMap* is valid.
    // </summary>
    //public event EventHandler ItemsAdded;

    protected override void ClearContainerForItemOverride(DependencyObject element, object item)
    {
        // let the base class do its thing
        base.ClearContainerForItemOverride(element, item);

        PerformLayoutAsync();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        base.IsItemItsOwnContainerOverride(item); // docs say to call base -- why?
        return false;
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        base.GetContainerForItemOverride(); // docs say to call base -- why?
        return new TouchListBoxItem();
    }

    public bool TrySetPseudofocus(TouchListBoxItem button)
    {
        return _layoutRoot.TrySetPseudofocus(button);
    }

    protected virtual void OnItemClick(object item, TouchListBoxItem container)
    {
        //_layoutRoot.PseudofocusedItem = container;
        Focus();
        if (_selectionMode == SelectionMode.SingleSelect)
            SelectedItemValue = container.ItemValue;

        if (_selectionMode != SelectionMode.None)
        {
            // perform the action specified by <ItemAction> on this control and item data on the
            // clicked-on item
            string data = Item.GetUntypedValue(item);
            if (data != null)
                _syncHelper.PerformItemAction(ItemAction, data, this);
        }

        // fire the ItemClick event
        if (ItemClick != null)
        {
            ItemClick(this, new ItemClickEventArgs()
            {
                Item = item,
                ItemValue = container.ItemValue
            });
        }
    }

    protected override void OnDragZoneVisibilityChanged(DragZoneVisibility oldValue, DragZoneVisibility newValue)
    {
        if (_layoutRoot != null) // i.e. if OnApplyTemplate was called already
            PerformLayoutAsync();
    }

    protected override void OnListMarginChanged(Thickness oldValue, Thickness newValue)
    {
        if (_layoutRoot != null) // i.e. if OnApplyTemplate was called already
            PerformLayoutAsync();
    }

    protected override void OnSyncPathChanged(string oldValue, string newValue)
    {
        _syncHelper.SyncPath = SyncPathClass.FromString(newValue);
    }

    protected override void OnConfigurationSyncPathChanged(string oldValue, string newValue)
    {
        _syncHelper.ConfigurationSyncPath = SyncPathClass.FromString(newValue);
    }

    protected override void OnItemsConfigurationSyncPathChanged(string oldValue, string newValue)
    {
        PerformLayoutAsync();
    }

    protected override void OnSelectedItemValueChanged(object oldValue, object newValue)
    {
        if (_layoutRoot != null) // i.e. if OnApplyTemplate was called already
        {
            // ensure that <newValue> is valid for the ItemAction of this list box
            if ((newValue != null) &&
                (Item.TypedValueToUntypedValue(ItemAction, newValue) == null))
            {
                SelectedItemValue = null;
                throw new InvalidOperationException(String.Format(
                    "TouchListBox.SelectedItemValue cannot be set to a value of type \"{0}\" when ItemAction is \"{1}\"",
                    newValue.GetType(), ItemAction));
            }
            UpdateSelection(oldValue, newValue);
        }
    }

    protected override void OnIsWaitingChanged(bool oldValue, bool newValue)
    {
        if (_layoutRoot != null) // i.e. if OnApplyTemplate was called already
        {
            _layoutRoot.IsWaiting = newValue;

            // fire the IsWaitingChanged event
            if (IsWaitingChanged != null)
            {
                IsWaitingChanged(this, new IsWaitingChangedEventArgs()
                {
                    IsWaiting = newValue
                });
            }
        }
    }

    void UpdateSelection(object oldItemValue, object newItemValue)
	{
        if (_selectionMode != SelectionMode.MultiSelect)
        {
            // turn off IsLit for the previously-selected item (if any)
            if (oldItemValue != null)
            {
                TouchListBoxItem oldItemContainer;
                if (_mapItemValueToContainer.TryGetValue(oldItemValue, out oldItemContainer))
                    oldItemContainer.IsLit = false;
            }

            // turn on IsLit for the newly-selected item (if any)
            TouchListBoxItem newItemContainer;
            if (newItemValue != null)
            {
                if (_mapItemValueToContainer.TryGetValue(newItemValue, out newItemContainer))
                    newItemContainer.IsLit = true;
            }
            else
                newItemContainer = null;

            // set pseudofocus to the newly-selected item (if any)
            //_layoutRoot.PseudofocusedItem = newItemContainer;
            _layoutRoot.TrySetPseudofocus(newItemContainer);
        }

		// fire the SelectedItemValueChanged event
		if (SelectedItemValueChanged != null)
			SelectedItemValueChanged(this, EventArgs.Empty);
	}

    double _extentLayedOutWidth = Double.NaN;

    void PerformLayoutIfExtentWidthChanged()
    {
        if (_extent.ActualWidth != _extentLayedOutWidth)
            PerformLayout();
    }

	void PerformLayout()
	{
        _pendingLayout = false;

        if (_rowContainer == null)
        {
            // it's too early to be performing layout -- and, in this case, layout will be
            // automatically attempted again at a later time
            return;
        }

        if (_mapItemToContainer.Count != Items.Count)
        {
            // We're in the middle of this sequence (when items are added to Items):
			//
			//     NotifyCollectionChangedAction.Reset
			//     NotifyCollectionChangedAction.Add (n times)
			//     <time passes>
			//     PrepareContainerForItemOverride (n times)
			//
			// ...or this sequence (when Items is cleared):
			//
			//     ClearContainerForItemOverride (n times)
			//     NotifyCollectionChangedAction.Reset
			//
			// In any case, since <_mapItemToContainer> isn't sync'd up with <Items>, we can't do
            // layout yet, so delay it.
			PerformLayoutAsync();
            return;
        }

		Thickness listMargin = ListMargin;
		_gripBlockContainer.Children.Clear();
		_rowContainer.Children.Clear();
		double cx = _extent.ActualWidth;
		double x = 0;
		double y = listMargin.Top;
		double yRowBottom = 0;
		TouchListBoxRow row = null;
		char displayedLetter = '\0';
		GripBlock gripBlock = null;
        DragZoneVisibility dragZoneVisibility = DragZoneVisibility;
		foreach (object item in Items)
		{
			TouchListBoxItem itemContainer = _mapItemToContainer[item];
            itemContainer.Visibility = Visibility.Visible;
			if (x + itemContainer.Width > cx)
			{
				row = null;
				x = 0;
				y = yRowBottom;
			}
			if (row == null)
			{
				x += listMargin.Left;
				yRowBottom = y + itemContainer.Height;

                if (dragZoneVisibility != DragZoneVisibility.Hidden)
                {
                    gripBlock = new GripBlock()
                    {
                        Style = _gripBlockStyle,
                        HorizontalAlignment = HorizontalAlignment.Left,
                        VerticalAlignment = VerticalAlignment.Top,
                        Height = itemContainer.Height
                    };
                    gripBlock.SetValue(Canvas.LeftProperty, x);
                    gripBlock.SetValue(Canvas.TopProperty, y);
                    x += gripBlock.Width + listMargin.Left;
                    _gripBlockContainer.Children.Add(gripBlock);
                }

				row = new TouchListBoxRow()
				{
					HorizontalAlignment = HorizontalAlignment.Stretch,
					VerticalAlignment = VerticalAlignment.Top,
					Height = itemContainer.Height,
				};
				row.SetValue(Canvas.LeftProperty, x);
				row.SetValue(Canvas.TopProperty, y);
				_rowContainer.Children.Add(row);
			}

			row.Items.Add(itemContainer);
			itemContainer.Row = row;
			itemContainer.SetValue(Canvas.LeftProperty, x);
			itemContainer.SetValue(Canvas.TopProperty, y);

            if (dragZoneVisibility == DragZoneVisibility.VisibleWithIndex)
            {
                char letter, ch;
                FrameworkElement itemAsFrameworkElement;
                string itemAsString;
                TextBlock itemAsTextBlock;
                if (((itemAsFrameworkElement = item as FrameworkElement) != null) &
                    ((ch = TouchSelector.GetIndexLetter(itemAsFrameworkElement)) != '\0'))
				{
					letter = ch;
                }
                else
                if ((itemAsString = item as string) != null)
                {
                    if (itemAsString.Length > 0)
                        letter = itemAsString[0];
                    else
                        letter = default(char);
                }
                else
                if ((itemAsTextBlock = item as TextBlock) != null)
                {
                    string text = itemAsTextBlock.Text;
                    if (text.Length > 0)
                        letter = text[0];
                    else
                        letter = default(char);
                }
                else
                    letter = default(char);

                letter = Char.ToUpper(letter);

                if (Char.IsLetter(letter))
                {
                    if (displayedLetter != letter)
                    {
                        gripBlock.Label = new String(letter, 1);
                        displayedLetter = letter;
                    }
                }
            }

			x += itemContainer.Width;
		}
		y = yRowBottom + listMargin.Bottom;
		_extent.Height = y;

        _extentLayedOutWidth = cx;

        // ensure that some item has pseudofocus (unless there are no items)
		EnsurePseudofocus();

/*
		// in case <_selectedContainer> changed (e.g. items were refreshed), update display of
		// selection
        UpdateSelection(SelectedItemValue, false);
*/

        if (LayoutComplete != null)
            LayoutComplete(this, EventArgs.Empty);
	}

    /// <summary>
    /// Layout is complete for one or all of the items that have been added.
    /// (Use <r>TryMapItemToButton</r> and <r>TryMapItemValueToButton</r> to see if a specific
    /// item is included in this layout pass.)
    /// </summary>
    public event EventHandler LayoutComplete;

	// ensure that some item has pseudofocus (unless there are no items)
	void EnsurePseudofocus()
	{
        // do nothing if some item already has pseudofocus
        if (_layoutRoot.PseudofocusedItem != null)
			return;

        // if an item is selected, give it pseudofocus
		if (SelectedItemValue != null)
		{
			TouchListBoxItem container;
			if (_mapItemValueToContainer.TryGetValue(SelectedItemValue, out container))
			{
				//_layoutRoot.PseudofocusedItem = container;
                _layoutRoot.TrySetPseudofocus(container);
                return;
			}
        }

        // otherwise, give the first item (if any) pseudofocus
		if (Items.Count > 0)
		{
            object item = Items[0];
            TouchListBoxItem container = _mapItemToContainer[item];
            //_layoutRoot.PseudofocusedItem = container;
            _layoutRoot.TrySetPseudofocus(container);
            return;
		}
	}

    public bool ScrollIntoView(TouchListBoxItem button, ScrollAlignment alignment)
    {
        return _layoutRoot.ScrollIntoView(button, alignment);
    }
}

// only for internal use by TouchListBox
public class TouchListBoxScrollViewer : TouchScrollViewerWithFocus
{
    internal Canvas RowContainer { get; set; }

    public TouchListBoxScrollViewer()
    {
        DefaultStyleKey = typeof(TouchScrollViewer);
    }

    public override IList<FrameworkElement> Rows
    {
        get
        {
            UIElementCollection rows = RowContainer.Children;
            return new SimpleList<FrameworkElement>(rows.Count, i => (FrameworkElement)rows[i]);
        }
    }

    protected override int GetIndexOfRow(FrameworkElement row)
    {
        return RowContainer.Children.IndexOf(row);
    }

    protected override IList<IPseudofocusable> GetRowItems(FrameworkElement row)
    {
        var row2 = (TouchListBoxRow)row;
        List<TouchListBoxItem> items = row2.Items;
        return new SimpleList<IPseudofocusable>(items.Count, i => items[i]);
    }

    protected override FrameworkElement GetRowOfItem(IPseudofocusable item)
    {
        var item2 = (TouchListBoxItem)item;
        return item2.Row;
    }

    protected override bool ContainsRow(FrameworkElement row)
    {
        return row.Parent == RowContainer;
    }

    protected override void SetInitialPseudofocus()
    {
        // don't let the base class set initial focus -- instead, it's done by PerformLayout
    }
}

// only for internal use by TouchListBox
/// <summary>
/// TODO
/// </summary>
public class TouchListBoxItem : TouchButton
{
    internal TouchListBoxRow Row { get; set; }

    public object ItemValue { get; set; }

    public TouchListBoxItem()
    {
        IsTabStop = false;

        // we suppress checking for when this TouchButton is an orphan because, for some reason,
        // Parent is always null, which makes it appear to always be an orphan
        SuppressOrphanCheck = true;
    }
}

[TemplateVisualState(GroupName = CommonStatesName, Name = NormalName)]
[TemplateVisualState(GroupName = CommonStatesName, Name = DisabledName)]
[TemplateVisualState(GroupName = LabelStatesName, Name = LabelVisibleName)]
[TemplateVisualState(GroupName = LabelStatesName, Name = LabelHiddenName)]
public class GripBlock : Control
{
    const string LabelStatesName = "LabelStates";
    const string LabelVisibleName = "LabelVisible";
    const string LabelHiddenName = "LabelHidden";
    const string CommonStatesName = "CommonStates";
    const string NormalName = "Normal";
    const string DisabledName = "Disabled";

    public static readonly DependencyProperty LabelProperty =
        DependencyProperty.Register("Label", typeof(string), typeof(GripBlock),
        null);

    public string Label
    {
        get
        {
            return (string)GetValue(LabelProperty);
        }
        set
        {
            SetValue(LabelProperty, value);
        }
    }

    public GripBlock()
    {
        DefaultStyleKey = typeof(GripBlock);
        IsEnabledChanged += delegate { UpdateVisualState(true); };
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        // initialize the visual state
        UpdateVisualState(false);
    }

    void UpdateVisualState(bool useTransitions)
    {
        if (IsEnabled)
            VisualStateManager.GoToState(this, NormalName, useTransitions);
        else
            VisualStateManager.GoToState(this, DisabledName, useTransitions);

        if (!String.IsNullOrEmpty(Label))
            VisualStateManager.GoToState(this, LabelVisibleName, false);
    }
}

// only for internal use by TouchListBox
/// <summary>
/// TODO
/// </summary>
public class TouchListBoxRow : Control
{
    internal List<TouchListBoxItem> Items { get; set; }

    public TouchListBoxRow()
    {
        Items = new List<TouchListBoxItem>();
        IsTabStop = false;
    }
}

/// <summary>
/// TODO
/// </summary>
///
public enum DragZoneVisibility
{
    /// <summary>
    /// The drag zone is not visible.
    /// </summary>
    Hidden,

    /// <summary>
    /// The drag zone is visible.
    /// </summary>
    Visible,

    /// <summary>
    /// The drag zone is visible, and index characters are displayed.
    /// </summary>
    VisibleWithIndex
}

/// <summary>
/// TODO
/// </summary>
///
public enum SelectionMode
{
    /// <summary>
    /// Clicking an item deselects the previously-select item (if any) and selects the new item.
    /// </summary>
    SingleSelect,

    /// <summary>
    /// Clicking an item toggles its selection state.
    /// </summary>
    /// <remarks>
    /// How multi-select works: in <r>MultiSelect</r> mode, ItemAction="ToggleBooleanValue",
    /// ItemsConfigurationSyncPath to whatever provides item names (e.g. "." in the case of
	/// SyncPath="DeviceStatus", for example), and then, in multi-select mode, SelectedItemValue
	/// is not used, and instead each button (item) is lit or not depending on whether the
	/// SyncNode at path "(SyncPath)\(item-name)" has a value of <n>true</n> or <n>false</n>.
    /// </remarks >
    MultiSelect,

    /// <summary>
    /// Clicking an item does not affect selection.  Event handler(s) may choose to modify item
    /// state (e.g. <r>TouchListBoxItem</r>.<s>TouchButton.IsLit</s>) if desired.
    /// </summary>
    None
}

/// <summary>
/// TODO
/// </summary>
///
public class ItemClickEventArgs : EventArgs
{
    /// <summary>
    /// TODO
    /// </summary>
    ///
    public object Item { get; internal set; }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public object ItemValue { get; internal set; }
}

/// <summary>
/// TODO
/// </summary>
/// 
/// <param name="sender">TODO</param>
/// 
/// <param name="e">TODO</param>
public delegate void ItemClickEventHandler(object sender, ItemClickEventArgs e);

}
